iT邦幫忙

2022 iThome 鐵人賽

DAY 22
1
Web 3

Road Map To DApp Developer系列 第 22

【DAY22】 - Optimize Gas Fee

  • 分享至 

  • xImage
  •  

Preface

今天想要介紹如何在合約中優化 gas fee。因為在以太坊中的 gas fee 價錢實在是太高昂了,而 gas fee 的成本主要與合約設計本身有很大的關係。降低 gas fee 除了可節省項目方(開發者)付出的成本,也可以降低消費者(mint 者)所需要花費的金額

但其實降低成本時,很多時候也會產生另一層面的問題,例如將 variable 的大小改動時,可能要注意 overflow 的情況,這些情況對於開發者或消費者而言都是不樂見的,因此在使用時也需要非常小心。

會想要寫入鐵人是因為我自己不管是在做專案或是一般測試的時候,常常忽略掉一些小地方,導致最後的 gas fee 與原本預測的相差甚遠,因此想透過寫入文章的方式提醒我未來需要考慮的更周全

Variables ordering

solidity 在宣告變數的時候,同時也需要宣告其型別,因此在使用變數前可以思考這個變數會使用到多少的空間,有沒有可能將其大小縮減。

uint

在 solidity 中 uint 便是宣告一個 uint256 的變數。其中 256 的意義是可以存取多少 bit,uint256 的範圍就可以從 0 ~ 2^256 - 1、而像uint128 的範圍就是 0 ~ 2^128,以此類推。

用 solidity 內建的 type 查詢可以看到:

function getMax() public pure returns(uint256, uint128, uint64, uint32, uint16, uint8) {
    return (
        type(uint256).max,
        type(uint128).max,
        type(uint64).max,
        type(uint32).max,
        type(uint16).max,
        type(uint8).max
    );
}

>> uint256: 115792089237316195423570985008687907853269984665640564039457584007913129639935
>> uint128: 340282366920938463463374607431768211455
>> uint64: 18446744073709551615
>> uint32: 4294967295
>> uint16: 65535
>> uint8: 255

因此在宣告變數的時候可以先想想「這個函數需要做什麼?」、或是「他有可能遇到什麼樣的狀況?」等情況去發想來做設計。

例如今天要宣告一個 TOTAL_SUPPLY 的全域變數,而你實際的 NFT 供給量只有 1000 個,這時你可以用 uint16 而不是直接宣告一個 uint256 來當作它的型別。

tokenId

雖然平常時可以將 uint 設置成自己想要的大小,但是還是會有例外的!(其實也不算是例外,只是要了解設置背後的意義是什麼?

在寫 BAKAJOHN 還有其他 NFT 相關智能合約時,我常常遇到一個問題 -- 「為什麼 tokenId 需要用一個 uint256 來存?」。原因是,我明明就最多只有 total_supply 個 NFT 被 mint,但為什麼需要付出這麼多額外的 storage 來存這個變數,豈不是浪費了嗎?

查詢 StackOverflow 查到了一個相關的問題解答了我。

其實 tokenId 不需要是連續整數阿!

The choice of uint256 allows a wide variety of applications because UUIDs and sha3 hashes are directly convertible to uint256.

在一些使用情境,項目方可以將 tokenId 設置成一個 hash 值,在這些情況,因為 hash 可以直接存入一個 uint256 裡面,因此會更適合拿來使用。

但是這樣的 tokenId 可能便需要配合 metadata 的檔名,才能讓 Opensea 取得。

Loop

另外,配合著 variable 的內容,不得不提到一個在程式中重要的機制 -- LOOP

在很多時候我們都需要使用到 for loop,但是 for loop 可能會需要花費較多的 gas fee 來做到同樣的事情。

先前在 【DAY18】 時就提到嘟嘟房使用的 whitelist 是用個 array 來包起來。這時他們要去存取一個 address 最多要花 O(n) 的時間,也就是用 for loop 從 0 跑到 n-1 的狀況。

因此這時則需要使用 mapping 會是個較好的選擇。

在很多時候會使用 mapping 作為查詢的工具,而 array 則多會使用在儲存一些其他的變數,像是在 Dynamic NFT 中使用的 string URI[3] = ["CID1", "CID2", "CID3"] 等。

Minimize on-chain data & Operation

我們都知道在鏈上儲存一個東西,在 EVM 中會呼叫 SSTORE 這個 OPCODE,在這邊可以看到:

儲存一個 variable(256 bits) 最多會花費 20000 gas,而若是儲存太多資料(例:整棵 Merkle Tree)在鏈上將會耗費超大量的 gas fee,且運作的時候將會非常笨重。

因此在鏈上儲存一些東西時,會使用其他的選擇來避免直接儲存在鏈上。

IPFS

前面有提到的 metadata 多會儲存在 IPFS 中,這方面我就不多贅述(見 DAY7)。

除了 NFT 的 metadata 以外,其實也有很多東西適合儲存在 IPFS 中。

例如在 9 月中我參加了一個黑客松,我們的作品是一個 Community 的 DAO,這個作品主要是可以驗證大家擁有的 NFT,驗證過後可以讓合格的使用者提出提案(就像是 EIP 一樣)。

而我們在提案的時候需要使用者提供:

  1. 提案標題
  2. 提案簡述內容(限制字數)
  3. 提案 pdf link

事實上,我們在比賽途中有提到是否可以使用 IPFS 當作儲存方法,但最後因為時間緣故沒有實做出來。

當時的想法是

前端 --(information)-> web3.storage(IPFS) -> Write in Smart Contract

在 IPFS 上儲存後會產生一個 CID,這個 CID 是個固定長度的 string(因為是透過 hash() function 產生),而透過這個 CID 便可以用各式各樣的 gateway 來 fetch 這些資料。

在這個使用情境下,透過 IPFS 將 CID 儲存在鏈上會遠比直接將資料儲存在鏈上的方式還要好;但這也可能產生另一層面的問題 -- 需要做出其他額外的方式來 fetch API 等,這方面就不再多贅述了。

Merkle Proof

前幾天介紹了 Merkle Proof 的使用情景與他的驗證方式,因此我們可以了解在鏈上維持一棵 Merkle Tree 似乎是個不明智的選擇。

在鏈上維持一棵樹的代價有:

  1. 儲存非常多的資料: 我們需要將一棵樹用一個 array 儲存,再利用 hash() 將其一步步的雜湊上去,同時也需要用其他的 array 來儲存這些 hash 值。

  2. insertion or deletion: 由於不是使用 mapping 來存,無法直接用 mapping(address => hash(address)) 的方式來存取,因此也只能用 iteration 的方式來找到 element 再插入或刪除。

由於以上因素,實作的時候通常不會將 Merkle Tree 放在鏈上,而是會在鏈下(off-chain)進行各種運算,由於是鏈下運算,同時連 proof 也可以一起產出

最後,在鏈上唯一需要儲存的東西剩下 merkle tree 的 root,並配合鏈下產生的 proof 與 leaf 一同做驗證的功能。

Closing

以上是我看到別人的文章並融合了自身開發經驗所做出的一些心得,但不代表完全正確,像是我也看到一篇文章提出 variable ordering 並不值得花太多的時間與精力,因為節省的效果很低(約 $1~2 美左右)。但我認為對於一個學習者而言,更嚴謹的對待自己寫的每一行合約/ code 才能讓自己成長,並在未來遇到類似問題(like overflow)的時候才能夠清楚的理解是哪裡出了錯,所以多用點心還是不虧的吧!

明天會分享關於盲盒與 mint 相關的內容!

Ref


上一篇
【DAY21】 - Verify with signature
下一篇
【DAY23】 - Blind Box and Verify
系列文
Road Map To DApp Developer30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言